import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.BitSet;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Vector;


public class FileHandler2 {

	private TorrentFile tfile;
	private String fileName;
	private Bits verifiedPieces;
	private Bits requestedPieces;
	private int numPieces;
	private RandomAccessFile file;
	private Piece[] incompletePieces;
	private MessageDigest md;
	private GlobalTorrentInfo info;
	private LinkedList<Integer> havesToSend;
	private Vector<Integer> partialPieces;

	public FileHandler2 (String torrentFileName, String dataFileName, TorrentFile tfile, GlobalTorrentInfo info) {
		this.tfile = tfile;
		this.numPieces = (int) Math.ceil((double)tfile.file_length / tfile.piece_length);
		System.out.println("mynumpieces: "+numPieces);

		if ((torrentFileName.length() < 9) || !torrentFileName.substring(
				torrentFileName.length() - 8).equals(".torrent")) {
			throw new IllegalArgumentException ("Torrent file names must end in \".torrent\"");
		}

		this.fileName = dataFileName;
		File f = new File (dataFileName);
		boolean existed = f.exists();

		try {
			this.file = new RandomAccessFile (f, "rw");
			this.file.setLength(tfile.file_length);
		} catch (Exception e) {
			System.err.println(e.getMessage());
			e.printStackTrace();
			System.exit(1);
		}

		this.verifiedPieces = new Bits (numPieces);
		System.out.println("verifiedpieces size: "+verifiedPieces.size());


		try {
			this.md = MessageDigest.getInstance("SHA-1");
		} catch (NoSuchAlgorithmException e) {
			System.exit(1);
		}

		try {
			if (existed) {
				if (verifyEntireFile())
					info.setSeed();
				info.tracker.decreaseBytesLeftBy(verifiedPieces.cardinality() * tfile.piece_length);
			}

		} catch (IOException e) {}

//		instantiate requestedPieces
		this.incompletePieces = new Piece [numPieces];
//		where is the lastblk lastp inf???

		this.info = info;
		info.dataFileHandler = this;
		this.requestedPieces = new Bits (numPieces);
		this.havesToSend = new LinkedList<Integer> ();
		this.partialPieces = new Vector<Integer>();
	}

	private byte[] sha1 (byte[] piece) {
		md.reset();
		byte[] sha1hash = md.digest(piece);
		assert(sha1hash.length == 20);
		return sha1hash;
	}

	private synchronized boolean verifyEntireFile () throws IOException {
		boolean verified = true;
		int pieces = 0;
		for (int k=0; k<numPieces; k++) {
			long pos = k * tfile.piece_length;
			byte[] piece = new byte[(int) Math.min(tfile.piece_length, tfile.file_length - pos)];
			file.seek(k * tfile.piece_length);
			file.read(piece);
			byte[] sha1hash = sha1(piece);
			byte[] pieceHash = (byte[]) tfile.piece_hash_values_as_binary.get(k);
			if (Arrays.equals(sha1hash, pieceHash)) {
				verifiedPieces.set(k);
				pieces++;
			}
			else
				verified = false;
		}
		System.out.println(pieces + " of " +numPieces + " verified");
		return verified;
	}

	private boolean validRequest (BlockInfo block) {
		if (block.index() >= numPieces) {
			System.out.println("Piece index greater than # total pieces");
			return false;
		}
		if (block.offset() + block.index()*tfile.piece_length + block.length() > tfile.file_length) {
			System.out.println("Block extends past filelength");
			return false;
		}
		if (block.offset() % 16384 != 0) {
			System.out.println("Block pieces/requests must be 16kB aligned");
			return false;
		}
		if (block.offset() + block.length() > tfile.piece_length) {
			System.out.println("Block extends past piece");
			return false;
		}
		return true;
	}

	public byte[] read (BlockInfo block) throws IOException {
		if (!verifiedPieces.get(block.index())) { // check if piece is verified
			System.out.println("Piece ("+block.index()+") not verified");
			return null;
		}
		if (!validRequest(block))
			return null;
		long fileOffset = block.offset() + block.index()*tfile.piece_length;
		// assertion: the specified block is contained within the verified piece
		byte[] returnData = new byte[block.length()];
		synchronized (file) {
			file.seek(fileOffset);
			file.readFully(returnData);
		}
		return returnData;
	}

	public synchronized boolean write (BlockInfo block, byte[] data) {
		if (block.index() == numPieces-1)
			System.out.println();
		if (verifiedPieces.get(block.index())) {
			if (GlobalTorrentInfo.debugBlocks)
				System.out.println("Piece " + block.index() + " already verified, block not written");
			return false;
		}
		if (!validRequest(block))
			return false;
		if (data.length != block.length()) {
			throw new IllegalArgumentException ("Block/data length mismatch");
		}
		long fileOffset = block.offset() + block.index()*tfile.piece_length;
		Piece p = incompletePieces[block.index()];
		if (p == null) {
			p = new Piece (block.index());
			incompletePieces[block.index()] = p;
		}
		if (!p.checkValidBlockLength(block) || p.isWritten(block))
			return false;
		synchronized (file) {
			try {
				file.seek(fileOffset);
				file.write(data);
			} catch (IOException e) {
				return false;
			}
		}
		p.markAsWritten(block);
		if (p.isPieceFull()) {
			partialPieces.remove(new Integer(block.index()));

			boolean verified = false;
			try {
				verified = p.verifyPiece();
			} catch (IOException e) {}
			if (verified) {
				info.tracker.decreaseBytesLeftBy(p.pieceSize);
				verifiedPieces.set(block.index(), true);
				requestedPieces.set(block.index(), true);
				incompletePieces[block.index()] = null;
				havesToSend.add((Integer) block.index());
				if(verifiedPieces.cardinality() == numPieces) {
					// we have whole file
					completed();
				}
			} else {
				p.resetPiece();
			}
		}
		incompletePieces[block.index()] = p;
		return true;
	}

	public boolean completedPiece (int pieceIndex) {
		return verifiedPieces.get(pieceIndex);
	}

	private void completed () {
		System.out.println("verifying file");
		boolean verified = false;
		try {
			verified = verifyEntireFile();
		} catch (IOException e) {}
		if (!verified) {
			System.err.println("Entire file not verified...god hates you");
			System.exit(1);
		}
		System.out.println("finished " + fileName);
		info.setSeed();
		info.tracker.completed();
	}

	boolean randomPiece = true;

	public synchronized BlockInfo nextBlock (BitSet remoteSet) {
		Bits remote = new Bits (remoteSet, numPieces);
		Bits possible;
		if (remote.cardinality() == 0)
			return null;    // no pieces you want
		while(true) {
			if (requestedPieces.cardinality() >= 15) {
				requestedPieces.clear();
			}
			remote = new Bits (remoteSet, numPieces);
			remote.andNot(verifiedPieces);
			possible = remote.clone();
			possible.andNot(requestedPieces);

			int pieceIndex = 0;
			if(randomPiece){
				pieceIndex = possible.randomSetBit();
				boolean newPiece = true;
				if(!partialPieces.isEmpty()){	//if there are partially downloaded pieces
					int rand = (int)(Math.random()*(partialPieces.size()-1));
					int index;
					for(int i = 0; i <partialPieces.size() ; i++){
						index = (i+rand)%partialPieces.size();
						if(possible.get(partialPieces.elementAt(index))){	//if it is possible
							pieceIndex = partialPieces.elementAt(index);	//download from one of them
							newPiece = false;
							break;
						}
					}

				}
				if(newPiece){	
					if(partialPieces.size()>4)
						resetRequests();
					partialPieces.add(new Integer(pieceIndex));
					if (GlobalTorrentInfo.debugBlocks)
						System.out.println("Partial downloading pieces: " + partialPieces.toString());				
				}
			} else {
				pieceIndex = possible.nextSetBit(0);
			}
			if (pieceIndex >= 0) {
				Piece p = incompletePieces[pieceIndex];
				if(p == null) {
					p = new Piece((Integer) pieceIndex);
					incompletePieces[pieceIndex] = p;
				}
				int block = p.nextRequest();
				if (block >= 0)
					return p.request(block);
			}
			else try {
				resetRequests();
			} catch (ArrayIndexOutOfBoundsException e) {return null;}
		}
	}

	public void resetRequests() {
		requestedPieces = verifiedPieces.clone();
		for(int i = 0; i<partialPieces.size(); i++){
			Piece p = incompletePieces[partialPieces.elementAt(i)];
			p.requestedBlocks = p.downloadedBlocks.clone();
		}
	}

//	public void timeout(BlockInfo block) {
//	requestedPieces.set(block.index(), false);
//	incompletePieces.get((Integer) block.index()).requestedBlocks.set((int) Math.ceil(block.offset()/16384));

//	}

	public BitSet getValidPieces () {
		return verifiedPieces.bitSet();
	}

	public String printValidPieces() {
		return verifiedPieces.toString();
	}

	public float printProgress() {
		return (float) verifiedPieces.cardinality() * 100 / verifiedPieces.size();
	}

	private class Piece {

		private Bits downloadedBlocks;
		private Bits requestedBlocks;
		private int numBlocks;  // # of 16kB blocks in this piece
		private int pieceIndex;
		private int pieceSize;

		private Piece (int pieceIndex) {
			this.pieceIndex = pieceIndex;
			if (pieceIndex == numPieces - 1)    // is this piece the last piece?
				pieceSize = tfile.file_length - (tfile.piece_length*pieceIndex);
			else
				pieceSize = tfile.piece_length;
			numBlocks = (int) Math.ceil((double)pieceSize / 16384);
			downloadedBlocks = new Bits (numBlocks);
			requestedBlocks = new Bits (numBlocks);
		}

		public int hashCode () {
			return pieceIndex;
		}

		private boolean checkValidBlockLength (BlockInfo block) {
			boolean lastPiece = (block.index() == numPieces - 1);
			if (lastPiece) {
				boolean lastBlock = (block.offset()/16384 == numBlocks-1);
				if (lastBlock) {
					int sizeOfLastBlock = pieceSize % 16384;
					return sizeOfLastBlock == block.length();
				}
			}
			return block.length() == 16384;
		}

		public String toString () {
			return downloadedBlocks + ((Boolean) verifiedPieces.get(pieceIndex)).toString() + "\n" + requestedBlocks + ((Boolean) requestedPieces.get(pieceIndex)).toString();
		}

		private boolean isWritten (BlockInfo block) {
			return downloadedBlocks.get(block.offset() / 16384);
		}

		private void markAsWritten (BlockInfo block) {
			int blockNum = block.offset() / 16384;
			downloadedBlocks.set(blockNum);
		}

		private void resetPiece () {
			downloadedBlocks.clear();
			requestedBlocks.clear();
		}

		private int nextRequest() {
			for (int k=0; k<numBlocks; k++) {
				if (!downloadedBlocks.get(k) && !requestedBlocks.get(k))
					return k;
			}
			requestedBlocks = downloadedBlocks.clone();
			return -1;
		}

		public BlockInfo request (int blockNum) {
			int offset = blockNum * 16384;
			if (offset > pieceSize)
				return null;
			int length = pieceSize - offset;
			if (length > 16384)
				length = 16384;
			requestedBlocks.set(blockNum);
			if (requestedBlocks.cardinality() >= numBlocks) {
				requestedPieces.set(pieceIndex);
				requestedBlocks = downloadedBlocks.clone();
			}
			return new BlockInfo (pieceIndex, offset, length);
		}

		public boolean equals (Object obj) {
			if (!(obj instanceof Piece))
				return false;
			return ((Piece) obj).pieceIndex == this.pieceIndex;
		}

		private boolean isPieceFull () {
			int c = downloadedBlocks.cardinality();
			if (GlobalTorrentInfo.debugBlocks)
				System.out.println(downloadedBlocks+" "+pieceIndex);
			return numBlocks == c;
		}

		private boolean verifyPiece () throws IOException {
			byte[] entirePiece = new byte[pieceSize];
			synchronized (file) {
				file.seek(pieceIndex * tfile.piece_length);
				file.readFully(entirePiece);
			}
			byte[] sha1hash = sha1 (entirePiece);
			boolean result = Arrays.equals((byte[]) tfile.piece_hash_values_as_binary.
					get(pieceIndex), sha1hash);
			if (result && GlobalTorrentInfo.debugBlocks)
				System.out.println("verified:"+pieceIndex);
			return result;
		}
	}

	public synchronized Integer getNextHave () {
		try {
			return havesToSend.remove();
		} catch (NoSuchElementException e) {
			return null;
		}
	}

//	public static void main (String[] args) throws IOException {
//	String fname = "project3.torrent";
//	TorrentFile tfile = (new TorrentFileHandler ()).openTorrentFile(fname);
//	FileHandler2 file = new FileHandler2(fname, tfile);

//	}

	@SuppressWarnings("serial")
	public static class PBitSet extends BitSet {
		public PBitSet () {
			super();
		}
		public PBitSet (int arg) {
			super(arg);
		}
		public String toString () {
			StringBuilder str = new StringBuilder();
			for (int k=0; k<this.size(); k++) {
				if (get(k))
					str.append('x');
				else
					str.append('_');
			}
			return new String (str);
		}
	}

}
